<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
</head>
<body>
  <svg width="300" height="300" id="svg"> 
    <rect width="100%" height="100%" fill="#ddd" />
  </svg>
  <script>
  // Common Defines
  var Dir = {UP:0,DOWN:1,LEFT:2,RIGHT:3};
  function $(id){return document.getElementById(id);};
  function C(tagname){return document.createElementNS('http://www.w3.org/2000/svg',tagname);};

  // == Class SnakeGame
  function SnakeGame() {
    this.MAX_COL = this.MAX_ROW = 10;
    this.snakeBody = [{r:0,c:0},{r:1,c:0},{r:2,c:0}];
    this.generateFood();
  }
  // == SnakeGame member functions
  // @param name - should be in ['oneatfood', 'onmove']
  SnakeGame.prototype.setListener = function(name, callback) { this[name] = callback; }
  // Core control logic of the game
  SnakeGame.prototype.move = function(dir) {
    var pos = this.tryMove(dir);
    // 1. If out of bound, or go into body, then Game Over! :)
    if (this.outOfBound(pos) || this.touchSelf(pos)) { return false; }
    // 2. If eat a food, lengthen snake and generate new food!
    if (this.touchFood(pos)) {
      this.snakeBody.push(pos);
      this.generateFood();
      this.oneatfood && this.oneatfood(pos, this.food);
      return true;
    }
    // 3. If nothing touched, move foward!
    this.snakeBody.push(pos);
    this.snakeBody.shift();
    this.onmove && this.onmove();
    return true;
  }
  // If move to this direction, where's the new head?
  SnakeGame.prototype.tryMove = function(dir) {
    var head = this.snakeBody[this.snakeBody.length-1];
    var pos = {r:head.r,c:head.c};
    if      (dir == Dir.UP)    {pos.r--}
    else if (dir == Dir.DOWN)  {pos.r++}
    else if (dir == Dir.LEFT)  {pos.c--}
    else if (dir == Dir.RIGHT) {pos.c++}
    return pos;
  }
  // Is out of bound
  SnakeGame.prototype.outOfBound = function(pos) {
    return pos.r<0 || pos.r>=this.MAX_ROW ||
           pos.c<0 || pos.c>=this.MAX_COL;
  }
  // Is touch food
  SnakeGame.prototype.touchFood = function(pos) {
    return this.food.r == pos.r && this.food.c == pos.c;
  }
  // Generate food randomly in empty space
  SnakeGame.prototype.generateFood = function() {
    if (this.snakeBody.length == this.MAX_COL * this.MAX_ROW) {return;}
    do {
      this.food = { 
        r: Math.round(Math.random()*this.MAX_ROW - 0.5),
        c: Math.round(Math.random()*this.MAX_COL - 0.5)
      }
    }while(this.touchSelf(this.food));
  }
  // Is get into snake body
  SnakeGame.prototype.touchSelf = function(pos) {
    for(var i=0; i<this.snakeBody.length; i++) {
      var spos = this.snakeBody[i];
      if (spos.r == pos.r && spos.c == pos.c) { return true; }
    }
    return false;
  }

  // == Class SvgView
  function SvgView(renderto,snakeGame) {
    this.svg = $(renderto);
    this.BlockSize = 100.0 / snakeGame.MAX_COL;

    // Create blocks for snake body, snake head and food
    this.snakeBlocks = [];
    for(var i=0; i < snakeGame.snakeBody.length; i++) {
      var color = (i == snakeGame.snakeBody.length-1) ? 'darkcyan' : 'black';
      var block = this.createBlock(snakeGame.snakeBody[i], color);
      this.svg.appendChild(block);
      this.snakeBlocks.push(block);
    }
    this.foodBlock = this.createBlock(snakeGame.food, '#5c5');
    this.svg.appendChild(this.foodBlock);

    // Bind listener for the game
    var self = this; // use 'self' instead of 'this' in callback functions!
    // When snake moves a step, update positions of all blocks
    snakeGame.setListener('onmove', function() {
      for(var i=0; i < snakeGame.snakeBody.length; i++) {
        self.setBlockPos(self.snakeBlocks[i], snakeGame.snakeBody[i]);
      }
    });
    // When eaten food, length snake blocks and update food position :) 
    snakeGame.setListener('oneatfood', function(pos, food) {
      var block = self.createBlock(snakeGame.snakeBody[snakeGame.snakeBody.length-2], 'black');
      var headBlock = self.snakeBlocks[self.snakeBlocks.length-1];
      self.svg.insertBefore(block, headBlock);
      self.snakeBlocks.splice(self.snakeBlocks.length-1,0,block);
      self.setBlockPos(headBlock, pos);
      self.setBlockPos(self.foodBlock, food);
    });
  }
  // == SvgView member functions
  // Create block for a position
  SvgView.prototype.createBlock = function(pos, color) {
    var block = C('circle');
    this.setBlockPos(block, pos);
    block.setAttribute('r', this.BlockSize*0.45 + '%');
    block.setAttribute('fill', color);
    return block;
  }
  // Set new position for an existing block
  SvgView.prototype.setBlockPos = function(block, pos) {
    block.setAttribute('cx', this.BlockSize*0.5 + pos.c * this.BlockSize + '%');
    block.setAttribute('cy', this.BlockSize*0.5 + pos.r * this.BlockSize + '%');
  }

  // == instances for the game and view
  var snakeGame = new SnakeGame();
  var svgView = new SvgView('svg', snakeGame);

  // == Key bindings
  document.onkeyup = function(e) {
    e = e || event;
    var keyCode = e.keyCode || e.which || e.charCode;
    //var keyName = String.fromCharCode(keyCode);
    //console.log(keyCode+" "+keyName);
    if      (keyCode == 37) {snakeGame.move(Dir.LEFT);}
    else if (keyCode == 38) {snakeGame.move(Dir.UP);}
    else if (keyCode == 39) {snakeGame.move(Dir.RIGHT);}
    else if (keyCode == 40) {snakeGame.move(Dir.DOWN);}
  }
  </script>
</body>
</html>
